[Swift 공식문서 읽기]Memory Safety

안녕하세요. 엘림입니다🙇🏻‍♀️

Swift 공식 문서를 정독하기 시리즈입니다!

제 스타일대로 정리했으니 추가적으로 더 필요한 정보는
공식문서 링크를 눌러 확인해주세용!

좀 더 편하게 보기위해 한국어로 번역된 사이트를 함께 확인했습니다!ㅎㅎ

자, 그럼 시작해볼까요

이 글은 공부하면서 작성한 글이기 때문에 잘못된 정보가 있을 수 있습니다.🥺
금방 잊어버릴... 미래의 저에게 다시 알려주기 위한 글이다보니
혹시라도 틀린 부분이 있다면, 댓글로 친절하게 알려주시길 부탁드립니다.🙏


메모리 안정성

기본적으로 Swift는 코드가 비정상적으로 동작하는 것을 막는 행위를 합니다. 예를 들어 변수가 초기화되기 전에 사용된다거나, 메모리에서 헤제된 값에 접근한다거나, 인덱스의 한계를 넘겨서 사용하려고 하는 것 등이 있습니다. 또한 Swift에서는 메모리의 위치를 수정하는 코드가 해당 메모리에 독점적으로 액세스할 수 있도록 요구함으로써, 동일한 메모리 영역에 대한 다중 액세스가 충돌하지 않도록 합니다.
이렇듯 메모리 관리를 자동으로 관리해주기 때문에 대부분의 경우에는 메모리 접근에 대해 생각하지 않아도 됩니다. 하지만 메모리 접근 충돌이 발생할 수 있는 잠재적인 상황을 이해하여, 메모리 접근 충돌을 피하는 코드를 어떻게 작성할 수 있는지 이해하는 것은 중요합니다.
(만약 메모리 접근 충돌이 일어나면 런타임 혹은 컴파일 에러가 발생합니다.)

메모리 접근 충돌의 이해

코드에서 메모리 접근은 변수에 값을 할당하거나, 접근할 때 발생합니다.

// A write access to the memory where one is stored.
var one = 1

// A read access from the memory where one is stored.
print("We're number \(one)!")

메모리에 대한 액세스 충돌은 코드의 다른 부분이 동시에 메모리의 동일한 위치에 액세스하려고 할 때 발생할 수 있습니다. 메모리의 한 위치에 동시에 여러번 접근하면, 예측할 수 없거나 일관성이 없는 동작이 발생할 수 있습니다. 즉, 메모리 충돌은 값을 할당하고, 접근하는 것을 동시에 수행할때 발생합니다.
(만약 동시성(cuncurrent) 코드나, 멀티쓰레드 코드를 작성한적이 있다면 이 메모리 접근 충돌 문제는 익숙한 문제일 것 입니다. 하지만 이 접근 충돌 문제는 싱글 쓰레드에서 발생할 수 있는 문제이고 동시성과 멀티쓰레드와 관련이 없습니다.)

메모리 접근의 특성

구체적인 메모리 충돌이 발생할 수 있는 상황은 다음 3가지 조건 중 2가지를 만족하면 발생합니다.

  • 최소 하나의 쓰기 접근 상황
  • 메모리의 같은 위치를 접근할 때
  • 접근 지속시간이 겹칠때

쓰기 접근은 메모리의 위치를 변경하고, 읽기는 그렇지 않습니다.

메모리의 위치는 무엇을 참조하고 있는지 나타냅니다.

메모리 접근의 지속시간은 즉각적인 접근과 장기 접근으로 구분할 수 있습니다.

즉각적인 접근은 코드에서 메모리 접근이 시작되고 끝나기 전에, 그 메모리에 대한 접근이 시작될 수 없음을 의미합니다.

  func oneMore(than number: Int) -> Int {
      return number + 1
  }

  var myNumber = 1
  myNumber = oneMore(than: myNumber)
  print(myNumber)
  // Prints "2"

위와 같은 경우가 즉각적인 접근이며, 이러한 경우 메모리 접근의 충돌이 발생하지 않습니다.
(대부분의 메모리 액세스는 즉각적입니다.)

반대로 장기 액세스는, 장기 액세스가 시작된 후 종료되기 전에 다른 코드가 실행될 수 있으며 이를 오버랩이라고 합니다.

in-out 파라미터의 충돌 접근

함수는 모든 in-out 매개변수에 대한 장기 쓰기 접근 권한을 가집니다.
in-out 매개변수에 대한 쓰기 액세스는 모든 non-in-out 매개변수가 평가된 후 시작되고 해당 함수 호출의 전체 기간 동안 지속됩니다. 인-아웃 매개변수가 여러 개인 경우 쓰기 액세스는 매개변수가 나타나는 순서대로 시작됩니다.

var stepSize = 1

func increment(_ number: inout Int) {
    number += stepSize
}

increment(&stepSize)
// Error: conflicting accesses to stepSize


increment의 파라미터로 inout Int의 number를 사용합니다. 그리고 함수 내부에서 인자로 사용한 number를 변경합니다. 이 경우 인자로 number를 넣고, 또 number를 읽어 그 number에 stepSize를 추가해 다시 할당하는 쓰기와 읽기가 동시에 발생해 접근 충돌이 일어 납니다.

// Make an explicit copy.
var copyOfStepSize = stepSize
increment(&copyOfStepSize)

// Update the original.
stepSize = copyOfStepSize
// stepSize is now 2

이 문제를 해결하기 위한 한가지 방법은 stepSize의 복사본을 명시적으로 사용하는 것입니다. stepSize를 복사한 copyOfStepSize를 사용하면 하나의 메모리를 읽고 쓰는 행위를 동시에 하지 않게돼 접근 충돌을 피할 수 있습니다.

func balance(_ x: inout Int, _ y: inout Int) {
    let sum = x + y
    x = sum / 2
    y = sum - x
}
var playerOneScore = 42
var playerTwoScore = 30
balance(&playerOneScore, &playerTwoScore)  // OK
balance(&playerOneScore, &playerOneScore)
// Error: conflicting accesses to playerOneScore

위와 같이 동일한 함수에 여러 in-out 매개변수에 대한 인수로, 같은 단일 변수를 전달하면 충돌이 발생합니다.
(연산자도 함수이기 때문에 in-out 파라미터 장기 접근으로 충돌이 발생합니다.)

메소드에서 self를 사용했을때의 충돌

구조체의 mutating 메서드의 self는 호출 기간 동안 쓰기 권한을 가집니다.

struct Player {
    var name: String
    var health: Int
    var energy: Int

    static let maxHealth = 10
    mutating func restoreHealth() {
        health = Player.maxHealth
    }
}

extension Player {
    mutating func shareHealth(with teammate: inout Player) {
        balance(&teammate.health, &health)
    }
}

var oscar = Player(name: "Oscar", health: 10, energy: 10)
var maria = Player(name: "Maria", health: 5, energy: 10)
oscar.shareHealth(with: &maria)  // OK


이 경우는 oscar와 maria둘다 다른 구조체 인스턴스 이기 때문에 체력을 공유해도 아래와 같이 아무 문제가 없습니다.

하지만 만약 oscar를 자신과 같은 인스턴스인 oscar와 체력을 공유한다고 실행하면 어떻게 될까요?

oscar.shareHealth(with: &oscar)
// Error: conflicting accesses to oscar


체력을 읽어오고 읽어온 체력을 변경하는 동작을 한 메모리 위치에서 동시에 수행하게 돼서 충돌이 발생합니다.

프로퍼티 접근 충돌

var playerInformation = (health: 10, energy: 20)
balance(&playerInformation.health, &playerInformation.energy)
// Error: conflicting access to properties of playerInformation

var holly = Player(name: "Holly", health: 10, energy: 10)
balance(&holly.health, &holly.energy)  // Error

func someFunction() {
    var oscar = Player(name: "Oscar", health: 10, energy: 10)
    balance(&oscar.health, &oscar.energy)  // OK
}

구조체, 튜플 및 열거와 같은 유형은 값 유형이기 때문에, 값 일부를 변경하면 전체 값이 변경됩니다. 그렇기 때문에 튜플의 요소를 지금처럼 같은 함수에 둘 다 in-out으로 호출하게되면, 중복 쓰기에 대한 접근이 발생하므로 충돌이 일어납니다.

(??? 공식문서 한번 더 확인해보기..!)

구조체에서 프로퍼티를 접근하는데, 오버래핑 접근으로부터 안전한 상황은 다음과 같습니다.

  • 구조체 인스턴스에서 저장프로퍼티만 접근하고, 계산된 프로퍼티 혹은 클래스 프로퍼티를 접근하지 않을 때
  • 구조체가 전역변수가 아니라 지역변수 일때
  • 구조체가 어떤 클로저로부터 캡쳐하지 않거나, nonescaping 클로저에서만 획득된 경우
    만약 컴파일러가 접근이 안전하지 않다고 판단하면, 접근 자체가 안됩니다.

오늘도 스위프트 공식문서를 정리해보았군욥~
다음편도 힘내보겠습니다!

감사합니다🙇🏻‍♀️

좋은 웹페이지 즐겨찾기